package com.epam.mycollections.lists;

/**
 * My Personal version of ArrayList
 *
 * @author Yuriy Miedviediev
 * @version 1.0 Build 05.04.2014
 *
 */

import com.epam.mycollections.interfaces.MyList;

import java.util.Arrays;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.RandomAccess;

public class MyArrayList<E> implements MyList<E>, RandomAccess {
    //container of all the data
    Object[] contents;
    //number of valuable elements in the ArrayList
    private int numElements = 0;

    private static final int DEFAULTCAPACITY = 10;
    private static final int HASHSALT = 31;
    private static final int FACTOR = 2;
    private static final int DIVISORFACTOR = 3;

    /**
     * Using no-argument constructor creates holder for data with initial capacity of 10
     */
    public MyArrayList() {
        this.contents = new Object[DEFAULTCAPACITY];
    }

    /**
     * Using this constructor creates holder for data with given initial capacity
     *
     * @param initialCapacity	nonnegative integer, initial capacity for ArrayList
     * @throws IllegalArgumentException		In case of negative number, throws an exception
     */
    public MyArrayList(int initialCapacity) {
        if(initialCapacity < 0) {
            throw new IllegalArgumentException("ArrayList cannot have negative capacity");
        }
        this.contents = new Object[initialCapacity];
    }

    // Helper method to verify and correct the size of ArrayList to be big enough to contain all elements
    private void ensureCapacity(int minCapacity) {

        //while size of ArrayList isn't big enough
        while(minCapacity > this.contents.length) {

            //new container with twice bigger size
            Object[] newContents = new Object[this.contents.length * FACTOR];

            //copy all elements from old container to the new one
            System.arraycopy(this.contents, 0, newContents, 0, this.contents.length);
            this.contents = newContents;
        }
    }

    /**
     * Add new element to the end of the ArrayList
     *
     * @param e 	new element of the list
     */
    @Override
    public void add(E e) {

        //first verify if current container can accommodate the element
        ensureCapacity(numElements+1);

        //place new element after last existing element
        this.contents[numElements] = e;

        //track the number of elements in the ArrayList
        this.numElements++;
    }

    /**
     * Add new element to a specified index of the ArrayList and shift all elements to the right
     * @param index		position to insert in
     * @param e		new element to add
     * @throws IllegalArgumentException		In case of incorrect index, throws an exception
     */
    @Override
    public void add(int index, E e) {

        //restrict using wrong indexes
        if(index > this.numElements || index < 0) {
            throw new MyIndexOutOfBoundsException();
        } else if(index == this.numElements) {
            this.add(e);
        } else {
            //temporary variables for shifting elements
            Object tempCurrent;
            Object tempNext;

            //verify the size of the container
            ensureCapacity(numElements+1);

            //remember old element and place the new one on its place
            tempCurrent = this.contents[index];
            this.contents[index] = e;

            //shift all elements to the right by one position
            for(int pos = index+1; pos<=this.numElements; pos++) {
                tempNext = this.contents[pos];
                this.contents[pos] = tempCurrent;
                tempCurrent = tempNext;
            }
            //track the number of elements
            this.numElements++;
        }
    }

    /**
     * Append all elements in the array to the end of the ArrayList
     * @param c		array of elements to append
     */
    @Override
    public void addAll(E[] c) {
        for(E element : c) {
            this.add(element);
        }
    }

    /**
     * Insert all elements of the array to the ArrayList starting at given index
     * @param index		index to start from
     * @param c		array of elements to insert
     */
    @Override
    public void addAll(int index, E[] c) {
        //restrict using wrong indexes
        if(index > this.numElements || index < 0) {
            throw new MyIndexOutOfBoundsException();
        }

        int insertIndex = index;
        for(E element : c) {
            this.add(insertIndex, element);
            insertIndex++;
        }
    }

    /**
     * Get element of the ArrayList by index
     * @param index		position of the element
     * @return 		element of the ArrayList
     */
    @Override
    public E get(int index) {
        //restrict using wrong indexes
        if(index >= this.numElements || index < 0) {
            throw new MyIndexOutOfBoundsException();
        } else {
            return (E)this.contents[index];
        }
    }

    /**
     * Set new value for the given position of the ArrayList
     * @param index
     * @param element
     * @throws IllegalArgumentException		In case of incorrect index, throws an exception
     */
    @Override
    public void set(int index, E element) {
        //restrict using wrong indexes
        if(index >= this.numElements || index < 0) {
            throw new MyIndexOutOfBoundsException();
        } else {
            this.contents[index] = element;
        }
    }

    /**
     * Return the number of elements in the ArrayList
     * @return	integer, size of the ArrayList
     */
    @Override
    public int size() {
        return this.numElements;
    }

    //Helper for adjusting the size of the container, reduce the number of empty cells if any
    private void trimToSize() {

        //if there exist empty cells
        if(this.numElements < this.contents.length) {

            //new container
            Object[] newContents = new Object[(this.contents.length * DIVISORFACTOR)/ FACTOR + 1];

            //copy all elements to the new container
            System.arraycopy(this.contents, 0, newContents, 0, this.numElements);
            this.contents = newContents;
        }
    }

    /**
     * Remove the element at given index, shift all elements to the left and adjust the size of the container
     * @param index		position in the ArrayList
     * @return	removed element
     */
    @Override
    public E remove(int index) {

        if(index >= this.numElements || index < 0) {
            throw new MyIndexOutOfBoundsException();
        }
        //remember removed element
        Object removed = this.contents[index];

        //temporary variables for shifting to the left
        Object tempCurrent = this.contents[numElements-1];
        Object tempPrevious;

        //shift every element to the left from the last element to the removed element
        for(int pos=this.numElements-FACTOR; pos>=index; pos--) {
            tempPrevious = this.contents[pos];
            this.contents[pos] = tempCurrent;
            tempCurrent = tempPrevious;
        }

        //track the number of elements
        this.numElements--;

        //adjust the size of the container
        this.trimToSize();

        return (E)removed;
    }

    @Override
    public boolean equals(Object otherObj) {
        if(!(otherObj instanceof MyList)) {
            return false;
        }
        MyList<E> list = (MyList<E>) otherObj;
        //if lists have different sizes, they are not equal
        if(this.size() != list.size()) {
            return false;
        }

        //check the equality of every element in the lists
        for(int pos=0, length=this.size(); pos<length; pos++) {
            E element = this.get(pos);
            E other = list.get(pos);

            if(element != null && other != null && !element.equals(other)) {
                return false;
            }
        }

        //if up to this moment none of the elements returned false, lists are equal
        return true;
    }

    @Override
    public int hashCode() {
        int result = contents != null ? Arrays.hashCode(contents) : 0;
        result = HASHSALT * result + numElements;
        return result;
    }

    /**
     * Find index of the element by its value
     * @param element desirable value
     * @return integer, position of the found value, or -1 otherwise
     */
    @Override
    public int indexOf(E element) {
        for(int pos=0, length=this.size(); pos<length; pos++) {
            E elem = (E)this.get(pos);
            if(elem.equals(element)) {
                return pos;
            }
        }
        return -1;
    }

    /**
     * Convert MyArrayList to a simple array
     * @return array of Objects
     */
    @Override
    public Object[] toArray() {
        return Arrays.copyOf(this.contents, this.contents.length);
    }

    /**
     * Clear all the contents in the current list
     */
    @Override
    public void clear() {
        this.contents = new Object[DEFAULTCAPACITY];
        this.numElements = 0;
    }

    /**
     * Check whether current list is empty
     */
    @Override
    public boolean isEmpty() {
        return this.numElements == 0;
    }

    /**
     * Get the standard iterator over the elements of the current list
     */
    @Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {

            private int pos = 0;

            @Override
            public boolean hasNext() {
                return pos <= numElements-1;
            }

            @Override
            public E next() {
                if(pos > numElements-1) {
                    throw new NoSuchElementException();
                }
                return (E)contents[pos++];
            }

            @Override
            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }
}
